﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace Camalot.Common.Membership {
	// http://codefirstmembership.codeplex.com
	// Microsoft Public License (Ms-PL)
	// 
  // This license governs use of the accompanying software. If you use the software, you accept this license. 
	// If you do not accept the license, do not use the software.
	// 
  // 1. Definitions
	//
	// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as 
	// under U.S. copyright law.
	// 
	// A "contribution" is the original software, or any additions or changes to the software.
	// 
	// A "contributor" is any person that distributes its contribution under this license.
	// 
	// "Licensed patents" are a contributor's patent claims that read directly on its contribution.
	// 
	// 2. Grant of Rights
	// 
	// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations 
	// in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to 
	// reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution 
	// or any derivative works that you create.
	// 
	// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in 
	// section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed 
	// patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution 
	// in the software or derivative works of the contribution in the software.
	// 
	// 3. Conditions and Limitations
	// 
	// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or 
	// trademarks.
	// 
	// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the 
	// software, your patent license from such contributor to the software ends automatically.
	// 
	// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and 
	// attribution notices that are present in the software.
	// 
	// (D) If you distribute any portion of the software in source code form, you may do so only under this license 
	// by including a complete copy of this license with your distribution. If you distribute any portion of the 
	// software in compiled or object code form, you may only do so under a license that complies with this license.
	// 
	// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express 
	// warranties, guarantees or conditions. You may have additional consumer rights under your local laws which 
	// this license cannot change. To the extent permitted under your local laws, the contributors exclude the 	
	// implied warranties of merchantability, fitness for a particular purpose and non-infringement.
	public class CodeFirstMembershipProvider : MembershipProvider{
		#region Properties

		public override string ApplicationName {
			get {
				return this.GetType ( ).Assembly.GetName ( ).Name.ToString ( );
			}
			set {
				this.ApplicationName = this.GetType ( ).Assembly.GetName ( ).Name.ToString ( );
			}
		}

		public override int MaxInvalidPasswordAttempts {
			get { return 5; }
		}

		public override int MinRequiredNonAlphanumericCharacters {
			get { return 0; }
		}

		public override int MinRequiredPasswordLength {
			get { return 6; }
		}

		public override int PasswordAttemptWindow {
			get { return 0; }
		}

		public override MembershipPasswordFormat PasswordFormat {
			get { return MembershipPasswordFormat.Hashed; }
		}

		public override string PasswordStrengthRegularExpression {
			get { return String.Empty; }
		}

		public override bool RequiresUniqueEmail {
			get { return true; }
		}

		#endregion

		#region Functions

		public override MembershipUser CreateUser ( string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status ) {
			if ( string.IsNullOrEmpty ( username ) ) {
				status = MembershipCreateStatus.InvalidUserName;
				return null;
			}
			if ( string.IsNullOrEmpty ( password ) ) {
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}
			if ( string.IsNullOrEmpty ( email ) ) {
				status = MembershipCreateStatus.InvalidEmail;
				return null;
			}

			string HashedPassword = Crypto.HashPassword ( password );
			if ( HashedPassword.Length > 128 ) {
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}

			using ( var Context = new DataContext ( ) ) {
				if ( Context.Users.Where ( Usr => Usr.Username == username ).Any ( ) ) {
					status = MembershipCreateStatus.DuplicateUserName;
					return null;
				}

				if ( Context.Users.Where ( Usr => Usr.Email == email ).Any ( ) ) {
					status = MembershipCreateStatus.DuplicateEmail;
					return null;
				}

				User NewUser = new User {
					UserId = Guid.NewGuid ( ),
					Username = username,
					Password = HashedPassword,
					IsApproved = isApproved,
					Email = email,
					CreateDate = DateTime.UtcNow,
					LastPasswordChangedDate = DateTime.UtcNow,
					PasswordFailuresSinceLastSuccess = 0,
					LastLoginDate = DateTime.UtcNow,
					LastActivityDate = DateTime.UtcNow,
					LastLockoutDate = DateTime.UtcNow,
					IsLockedOut = false,
					LastPasswordFailureDate = DateTime.UtcNow
				};

				Context.Users.Add ( NewUser );
				Context.SaveChanges ( );
				status = MembershipCreateStatus.Success;
				return new MembershipUser ( System.Web.Security.Membership.Provider.Name, NewUser.Username, NewUser.UserId, NewUser.Email, null, null, NewUser.IsApproved, NewUser.IsLockedOut, NewUser.CreateDate.Value, NewUser.LastLoginDate.Value, NewUser.LastActivityDate.Value, NewUser.LastPasswordChangedDate.Value, NewUser.LastLockoutDate.Value );
			}
		}

		public override bool ValidateUser ( string username, string password ) {
			if ( string.IsNullOrEmpty ( username ) ) {
				return false;
			}
			if ( string.IsNullOrEmpty ( password ) ) {
				return false;
			}
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Username == username );
				if ( User == null ) {
					return false;
				}
				if ( !User.IsApproved ) {
					return false;
				}
				if ( User.IsLockedOut ) {
					return false;
				}
				String HashedPassword = User.Password;
				Boolean VerificationSucceeded = ( HashedPassword != null && Crypto.VerifyHashedPassword ( HashedPassword, password ) );
				if ( VerificationSucceeded ) {
					User.PasswordFailuresSinceLastSuccess = 0;
					User.LastLoginDate = DateTime.UtcNow;
					User.LastActivityDate = DateTime.UtcNow;
				} else {
					int Failures = User.PasswordFailuresSinceLastSuccess;
					if ( Failures < MaxInvalidPasswordAttempts ) {
						User.PasswordFailuresSinceLastSuccess += 1;
						User.LastPasswordFailureDate = DateTime.UtcNow;
					} else if ( Failures >= MaxInvalidPasswordAttempts ) {
						User.LastPasswordFailureDate = DateTime.UtcNow;
						User.LastLockoutDate = DateTime.UtcNow;
						User.IsLockedOut = true;
					}
				}
				Context.SaveChanges ( );
				if ( VerificationSucceeded ) {
					return true;
				} else {
					return false;
				}
			}
		}

		public override MembershipUser GetUser ( string username, bool userIsOnline ) {
			if ( string.IsNullOrEmpty ( username ) ) {
				return null;
			}
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Username == username );
				if ( User != null ) {
					if ( userIsOnline ) {
						User.LastActivityDate = DateTime.UtcNow;
						Context.SaveChanges ( );
					}
					return new MembershipUser ( System.Web.Security.Membership.Provider.Name, User.Username, User.UserId, User.Email, null, null, User.IsApproved, User.IsLockedOut, User.CreateDate.Value, User.LastLoginDate.Value, User.LastActivityDate.Value, User.LastPasswordChangedDate.Value, User.LastLockoutDate.Value );
				} else {
					return null;
				}
			}
		}

		public override MembershipUser GetUser ( object providerUserKey, bool userIsOnline ) {
			if ( providerUserKey is Guid ) { } else {
				return null;
			}

			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.Find ( providerUserKey );
				if ( User != null ) {
					if ( userIsOnline ) {
						User.LastActivityDate = DateTime.UtcNow;
						Context.SaveChanges ( );
					}
					return new MembershipUser ( System.Web.Security.Membership.Provider.Name, User.Username, User.UserId, User.Email, null, null, User.IsApproved, User.IsLockedOut, User.CreateDate.Value, User.LastLoginDate.Value, User.LastActivityDate.Value, User.LastPasswordChangedDate.Value, User.LastLockoutDate.Value );
				} else {
					return null;
				}
			}
		}

		public override bool ChangePassword ( string username, string oldPassword, string newPassword ) {
			if ( string.IsNullOrEmpty ( username ) ) {
				return false;
			}
			if ( string.IsNullOrEmpty ( oldPassword ) ) {
				return false;
			}
			if ( string.IsNullOrEmpty ( newPassword ) ) {
				return false;
			}
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Username == username );
				if ( User == null ) {
					return false;
				}
				String HashedPassword = User.Password;
				Boolean VerificationSucceeded = ( HashedPassword != null && Crypto.VerifyHashedPassword ( HashedPassword, oldPassword ) );
				if ( VerificationSucceeded ) {
					User.PasswordFailuresSinceLastSuccess = 0;
				} else {
					int Failures = User.PasswordFailuresSinceLastSuccess;
					if ( Failures < MaxInvalidPasswordAttempts ) {
						User.PasswordFailuresSinceLastSuccess += 1;
						User.LastPasswordFailureDate = DateTime.UtcNow;
					} else if ( Failures >= MaxInvalidPasswordAttempts ) {
						User.LastPasswordFailureDate = DateTime.UtcNow;
						User.LastLockoutDate = DateTime.UtcNow;
						User.IsLockedOut = true;
					}
					Context.SaveChanges ( );
					return false;
				}
				String NewHashedPassword = Crypto.HashPassword ( newPassword );
				if ( NewHashedPassword.Length > 128 ) {
					return false;
				}
				User.Password = NewHashedPassword;
				User.LastPasswordChangedDate = DateTime.UtcNow;
				Context.SaveChanges ( );
				return true;
			}
		}

		public override bool UnlockUser ( string userName ) {
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Username == userName );
				if ( User != null ) {
					User.IsLockedOut = false;
					User.PasswordFailuresSinceLastSuccess = 0;
					Context.SaveChanges ( );
					return true;
				} else {
					return false;
				}
			}
		}

		public override int GetNumberOfUsersOnline ( ) {
			DateTime DateActive = DateTime.UtcNow.Subtract ( TimeSpan.FromMinutes ( Convert.ToDouble ( System.Web.Security.Membership.UserIsOnlineTimeWindow ) ) );
			using ( var Context = new DataContext ( ) ) {
				return Context.Users.Where ( Usr => Usr.LastActivityDate > DateActive ).Count ( );
			}
		}

		public override bool DeleteUser ( string username, bool deleteAllRelatedData ) {
			if ( string.IsNullOrEmpty ( username ) ) {
				return false;
			}
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Username == username );
				if ( User != null ) {
					Context.Users.Remove ( User );
					Context.SaveChanges ( );
					return true;
				} else {
					return false;
				}
			}
		}

		public override string GetUserNameByEmail ( string email ) {
			using ( var Context = new DataContext ( ) ) {
				User User = null;
				User = Context.Users.FirstOrDefault ( Usr => Usr.Email == email );
				if ( User != null ) {
					return User.Username;
				} else {
					return string.Empty;
				}
			}
		}

		public override MembershipUserCollection FindUsersByEmail ( string emailToMatch, int pageIndex, int pageSize, out int totalRecords ) {
			MembershipUserCollection MembershipUsers = new MembershipUserCollection ( );
			using ( var Context = new DataContext ( ) ) {
				totalRecords = Context.Users.Where ( Usr => Usr.Email == emailToMatch ).Count ( );
				IQueryable<User> Users = Context.Users.Where ( Usr => Usr.Email == emailToMatch ).OrderBy ( Usrn => Usrn.Username ).Skip ( pageIndex * pageSize ).Take ( pageSize );
				foreach ( User user in Users ) {
					MembershipUsers.Add ( new MembershipUser ( System.Web.Security.Membership.Provider.Name, user.Username, user.UserId, user.Email, null, null, user.IsApproved, user.IsLockedOut, user.CreateDate.Value, user.LastLoginDate.Value, user.LastActivityDate.Value, user.LastPasswordChangedDate.Value, user.LastLockoutDate.Value ) );
				}
			}
			return MembershipUsers;
		}

		public override MembershipUserCollection FindUsersByName ( string usernameToMatch, int pageIndex, int pageSize, out int totalRecords ) {
			MembershipUserCollection MembershipUsers = new MembershipUserCollection ( );
			using ( var Context = new DataContext ( ) ) {
				totalRecords = Context.Users.Where ( Usr => Usr.Username == usernameToMatch ).Count ( );
				IQueryable<User> Users = Context.Users.Where ( Usr => Usr.Username == usernameToMatch ).OrderBy ( Usrn => Usrn.Username ).Skip ( pageIndex * pageSize ).Take ( pageSize );
				foreach ( User user in Users ) {
					MembershipUsers.Add ( new MembershipUser ( System.Web.Security.Membership.Provider.Name, user.Username, user.UserId, user.Email, null, null, user.IsApproved, user.IsLockedOut, user.CreateDate.Value, user.LastLoginDate.Value, user.LastActivityDate.Value, user.LastPasswordChangedDate.Value, user.LastLockoutDate.Value ) );
				}
			}
			return MembershipUsers;
		}

		public override MembershipUserCollection GetAllUsers ( int pageIndex, int pageSize, out int totalRecords ) {
			MembershipUserCollection MembershipUsers = new MembershipUserCollection ( );
			using ( var Context = new DataContext ( ) ) {
				totalRecords = Context.Users.Count ( );
				IQueryable<User> Users = Context.Users.OrderBy ( Usrn => Usrn.Username ).Skip ( pageIndex * pageSize ).Take ( pageSize );
				foreach ( User user in Users ) {
					MembershipUsers.Add ( new MembershipUser ( System.Web.Security.Membership.Provider.Name, user.Username, user.UserId, user.Email, null, null, user.IsApproved, user.IsLockedOut, user.CreateDate.Value, user.LastLoginDate.Value, user.LastActivityDate.Value, user.LastPasswordChangedDate.Value, user.LastLockoutDate.Value ) );
				}
			}
			return MembershipUsers;
		}

		#endregion

		#region Not Supported

		//CodeFirstMembershipProvider does not support password retrieval scenarios.
		public override bool EnablePasswordRetrieval {
			get { return false; }
		}
		public override string GetPassword ( string username, string answer ) {
			throw new NotSupportedException ( "Consider using methods from WebSecurity module." );
		}

		//CodeFirstMembershipProvider does not support password reset scenarios.
		public override bool EnablePasswordReset {
			get { return false; }
		}
		public override string ResetPassword ( string username, string answer ) {
			throw new NotSupportedException ( "Consider using methods from WebSecurity module." );
		}

		//CodeFirstMembershipProvider does not support question and answer scenarios.
		public override bool RequiresQuestionAndAnswer {
			get { return false; }
		}
		public override bool ChangePasswordQuestionAndAnswer ( string username, string password, string newPasswordQuestion, string newPasswordAnswer ) {
			throw new NotSupportedException ( "Consider using methods from WebSecurity module." );
		}

		//CodeFirstMembershipProvider does not support UpdateUser because this method is useless.
		public override void UpdateUser ( MembershipUser user ) {
			throw new NotSupportedException ( );
		}

		#endregion
	}
}
